/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simhya.simengine.hybrid;

import cern.colt.Arrays;
import org.apache.commons.math.ode.sampling.StepHandler;
import org.apache.commons.math.ode.sampling.StepInterpolator;
import org.apache.commons.math.ode.DerivativeException;


import simhya.simengine.HybridSimulator;
import simhya.simengine.ProgressMonitor;
import simhya.simengine.ode.IntegratorType;

import simhya.model.flat.FlatModel;
import simhya.dataprocessing.DataCollector;
import simhya.dataprocessing.HybridDataCollector;
import simhya.simengine.utils.PrintStreamProgressMonitor;
import simhya.utils.RandomGenerator;
import cern.jet.random.engine.RandomEngine;
import simhya.GlobalOptions;

import org.apache.commons.math.ode.FirstOrderIntegrator;
import org.apache.commons.math.ode.events.EventHandler;
import java.util.ArrayList;
import simhya.model.store.Function;
import simhya.model.store.Predicate;
import simhya.model.transition.Transition;
import simhya.simengine.odeevents.*;
import simhya.simengine.SimulationException;
import java.util.HashSet;

/**
 *
 * @author luca
 */
public abstract class AbstractHybridSimulator implements HybridSimulator, StepHandler {
    FlatModel model;
    HybridOdeFunction function;
    FirstOrderIntegrator integrator;

    HybridDataCollector collector;
    
    long numberOfInstantaneousEvents;
    long numberOfTimedEvents;
    long numberOfStochasticEvents;
    long numberOfSteps;

    ProgressMonitor monitor;

    double [] variables;

    boolean useCache;
    boolean initialized;
    boolean allSwitchable;
    boolean depGraphInitialized;

    //simulation parameters.
    double initialTime;
    double finalTime;
    double absoluteTolerance;
    double relativeTolerance;
    double minStepSize;
    double maxStepSize;
    double maxStepIncrementForEvents;
    double maxErrorForEvents;
    int maxIterationforEvents;

    IntegratorType integratorType;
    RandomEngine rand;
    ArrayList<EventHandler> events;
    PartitionManager eventSwitcher;
    HashSet<Integer> eventIds;




    public AbstractHybridSimulator(FlatModel model, HybridDataCollector collector, boolean allSwitchable) {
        this.model = model;
        this.collector = collector;
        this.allSwitchable = allSwitchable;

        monitor = new PrintStreamProgressMonitor(System.out);
        rand = RandomGenerator.getNewRandomGenerator();

        this.absoluteTolerance = GlobalOptions.defaultAbsoluteTolerance;
        this.relativeTolerance = GlobalOptions.defaultRelativeTolerance;
        this.minStepSize = GlobalOptions.defaultMinStepSize;
        this.maxStepSize = GlobalOptions.defaultMaxStepSize;
        this.maxStepIncrementForEvents = GlobalOptions.defaultMaxStepIncrementForEvents;
        this.maxErrorForEvents = GlobalOptions.defaultMaxErrorForEvents;
        this.maxIterationforEvents = GlobalOptions.defaultMaxIterationforEvents;
        this.integratorType = GlobalOptions.defaultIntegratorType;
        this.initialized = false;
        depGraphInitialized = false;
        events = new ArrayList<EventHandler>();
        eventIds = new HashSet<Integer>();
        
        this.initializeOdeFunction();
        this.initializeEvents();
        this.initializeStochasticEvents();
  //      this.initializePartitionManager();
    }


    private void initializeOdeFunction() {
        if (model.containsStochasticGuardedTransitions(true))
            this.function = new GuardedHybridOdeFunction(model,allSwitchable);
        else
            this.function = new PureHybridOdeFunction(model,allSwitchable);
        this.variables = this.function.getVariables(model, rand);
    }



    /**
     * initializes stochastic events and adds them to the event list
     */
    private void initializeStochasticEvents() {
        if (this.allSwitchable) {
            for (Integer j : model.getListOfStochasticTransitionID(true)) {
                Transition t = model.getTransition(j);
                int variableID = function.getVariableOfStochasticEvent(j);
                MarkovianEvent e = new MarkovianEvent(j,function.dimension,variableID,t.getGuardPredicate(),rand,function);
                e.setReset(t.getResetList());
                e.setIsStopping(t.isStopping());
                e.setUseCache(useCache);
                e.setCollectorInfo(t.getEvent(), collector);
                this.events.add(e);
                this.eventIds.add(j);
            }
        } else {
            for (Integer j : model.getListOfStochasticTransitionID(false)) {
                Transition t = model.getTransition(j);
                int variableID = function.getVariableOfStochasticEvent(j);
                MarkovianEvent e = new MarkovianEvent(j,function.dimension,variableID,t.getGuardPredicate(),rand,function);
                e.setReset(t.getResetList());
                e.setIsStopping(t.isStopping());
                e.setUseCache(useCache);
                e.setCollectorInfo(t.getEvent(), collector);
                this.events.add(e);
                this.eventIds.add(j);
            }
            for (Integer j : model.getListOfHybridTransitionID()) {
                Transition t = model.getTransition(j);
                int variableID = function.getVariableOfStochasticEvent(j);
                MarkovianEvent e = new MarkovianEvent(j,function.dimension,variableID,t.getGuardPredicate(),rand,function);
                e.setReset(t.getResetList());
                e.setIsStopping(t.isStopping());
                e.setUseCache(useCache);
                e.setCollectorInfo(t.getEvent(), collector);
                this.events.add(e);
                this.eventIds.add(j);
            }
        }        
    }

    /**
     * Initializes timed and stochastic events and adds them to the event list.
     */
    private void initializeEvents() {
        for (Integer j : model.getListOfInstantaneousTransitionsID()) {
//            System.out.println("Initializing event of transition " + j);
            Transition t = model.getTransition(j);
//            System.out.println("Transition " );
//            System.out.println(t.getEvent());
            Predicate g = t.getGuardPredicate();
//            System.out.println(g.toModelLanguage());
            if (g.canBeConvertedToFunction()) {
//                if (j == 74)
//                    System.out.println("Sticazzi" );
                Function f = g.convertToFunction();
//                System.out.println(f.toModelLanguage());
//                System.out.println("Creating event " + j);
//                System.out.flush();
                InstantaneousEvent e = new InstantaneousEvent(j,function.dimension,g,f,t.getRateFunction(),function);
//                System.out.println("done");
                e.setReset(t.getResetList());
                e.setIsStopping(t.isStopping());
                e.setUseCache(useCache);
                e.setCollectorInfo(t.getEvent(), collector);
                this.events.add(e);
                this.eventIds.add(j);
//                System.out.println("ok");
            } else {
                throw new SimulationException("Guard of instantaneous transition " +
                        t.getEvent() + " cannot be converted to a function. " +
                        "It probably contains equality or inequality atomic predicates.");
            }
        }
        for (Integer j : model.getListOfTimedTransitionsID()) {
            Transition t = model.getTransition(j);
            Predicate g = t.getGuardPredicate();
            Function f = t.getTimedActivationFunction();
            if (function.evolvesContinuously(g) || function.evolvesContinuously(f)) {
                TimedEvent e = new TimedEvent(j,function.dimension,f,function);
                e.setGuard(g);
                e.setReset(t.getResetList());
                e.setIsStopping(t.isStopping());
                e.setUseCache(useCache);
                e.setCollectorInfo(t.getEvent(), collector);
                this.events.add(e);
                this.eventIds.add(j);
            } else {
                FixedTimedEvent e = new FixedTimedEvent(j,function.dimension,f,function);
                e.setGuard(g);
                e.setReset(t.getResetList());
                e.setIsStopping(t.isStopping());
                e.setUseCache(useCache);
                e.setCollectorInfo(t.getEvent(), collector);
                this.events.add(e);
                this.eventIds.add(j);
            }
        }

    }


    /**
     * TO IMPLEMENT
     */
    abstract void initializePartitionManager();

    abstract void initializeDependencyGraphs();
   


    public void handleStep(StepInterpolator interpolator, boolean isLast) throws DerivativeException {
        collector.setInterpolator(interpolator);
        numberOfSteps++;
        double time = interpolator.getCurrentTime();
        
        //System.out.println("Step handler; time is " + time  + " state is " + Arrays.toString(interpolator.getInterpolatedState()));
        
        if (isLast)
            collector.putFinalStateOde(time);
        else if (collector.dataNeeded(time,numberOfSteps))
            collector.putDataOde(time);
        monitor.setProgress(time);
    }

    public boolean requiresDenseOutput() {
        return true;
    }

    public void reset() {
        //initialization of step handler. We simply need to tell the data collector
        //to get ready to store a new trajectory.
        collector.newTrajectory();
    }


    public ProgressMonitor getProgressMonitor() {
        return this.monitor;
    }




    public long getLastSimulationNumberOfInstantaneousEvents() {
        return this.numberOfInstantaneousEvents;
    }

    public long getLastSimulationNumberOfStochasticEvents() {
        return this.numberOfStochasticEvents;
    }

    public long getLastSimulationNumberOfTimedEvents() {
        return this.numberOfTimedEvents;
    }

    public void setAbsoluteTolerance(double tol) {
        this.absoluteTolerance = tol;
    }

    public void setIntegrator(IntegratorType integ) {
        this.integratorType = integ;
    }

    public void setMaxErrorForEvents(double maxErrorForEvents) {
        this.maxErrorForEvents = maxErrorForEvents;
    }

    public void setMaxIterationforEvents(int maxIterationforEvents) {
        this.maxIterationforEvents = maxIterationforEvents;
    }

    public void setMaxStepIncrementForEvents(double maxStepIncrementForEvents) {
        this.maxStepIncrementForEvents = maxStepIncrementForEvents;
    }

    public void setMaximumStepSize(double stepSize) {
        if (stepSize >= this.minStepSize)
            this.maxStepSize = stepSize;
        else {
            this.maxStepSize = stepSize;
            this.minStepSize = stepSize;
        }
    }

    public void setMinimumStepSize(double stepSize) {
         if (stepSize <= this.maxStepSize)
            this.minStepSize = stepSize;
        else {
            this.maxStepSize = stepSize;
            this.minStepSize = stepSize;
        }
    }
    public void setRelativeTolerance(double tol) {
        this.relativeTolerance = tol;
    }

    public long getLastSimulationSteps() {
        return this.numberOfSteps;
    }

    public double getLastSimulationTimeInMillisecs() {
        return monitor.getLastSimulationTimeInMillisecs();
    }

    public double getLastSimulationTimeInSecs() {
        return monitor.getLastSimulationTimeInSecs();
    }



    public void reset(DataCollector dc) {
        if (!(dc instanceof HybridDataCollector))
            throw new IllegalArgumentException("Requires a OdeDataCollector");
        this.collector = (HybridDataCollector)dc;
        this.resetModel(true);

        rand = RandomGenerator.getNewRandomGenerator();

        this.absoluteTolerance = GlobalOptions.defaultAbsoluteTolerance;
        this.relativeTolerance = GlobalOptions.defaultRelativeTolerance;
        this.minStepSize = GlobalOptions.defaultMinStepSize;
        this.maxStepSize = GlobalOptions.defaultMaxStepSize;
        this.maxStepIncrementForEvents = GlobalOptions.defaultMaxStepIncrementForEvents;
        this.maxErrorForEvents = GlobalOptions.defaultMaxErrorForEvents;
        this.maxIterationforEvents = GlobalOptions.defaultMaxIterationforEvents;
        this.integratorType = GlobalOptions.defaultIntegratorType;
        this.initialized = false;
        depGraphInitialized = false;
        events = new ArrayList<EventHandler>();

        this.initializeOdeFunction();
        this.initializeEvents();
        this.initializeStochasticEvents();
        this.initializePartitionManager();
        this.initializeDependencyGraphs();
    }

    public void resetModel(boolean resetToOriginalValues) {
        if (resetToOriginalValues)
            model.resetToInitialState();
        this.variables = function.getVariables(model, rand);
    }

    public void run() {
        if (initialized) {
            //pass monitor to collector
            monitor.setInitialTime(initialTime);
            monitor.setFinalTime(finalTime);
            double[] y = new double[variables.length];
            function.setUseCache(useCache);
            monitor.start();
            try {
            integrator.integrate(function, initialTime, variables, finalTime, y);
            monitor.stop();
            }
            catch (org.apache.commons.math.ode.DerivativeException e) {
                throw new simhya.simengine.SimulationException("Error while " +
                        "computing derivatives: " + e );
            }
            catch (org.apache.commons.math.ode.IntegratorException e) {
                throw new simhya.simengine.SimulationException("Error while " +
                        "integrating ode: " + e );
            }
        }
    }

    public void setFinalTime(double time) {
        this.finalTime = time;
    }

    public void setInitialTime(double time) {
        this.initialTime = time;
    }

    public void setProgressMonitor(ProgressMonitor monitor) {
        if (monitor != null)
            this.monitor = monitor;
    }

    public void useChache(boolean useCache) {
        this.useCache = useCache;
    }

    FirstOrderIntegrator newIntegrator() {
        switch(this.integratorType) {
            case DP85:
                return new org.apache.commons.math.ode.nonstiff.DormandPrince853Integrator(minStepSize, maxStepSize, absoluteTolerance, relativeTolerance);
            case RK4:
                return new org.apache.commons.math.ode.nonstiff.ClassicalRungeKuttaIntegrator(minStepSize);
        }
        return null;
    }
    
    /**
     * Activate or deactivates the mechanism to print events.
     * @param print 
     */
    public void printEvents(boolean print) {
        for (EventHandler e : this.events) {
            if (e instanceof PrintableEvent) {
                ((PrintableEvent)e).setPrintEvent((print ? monitor : null));
            }
        }    
    }
    

}
